/*
 * Copyright 2000-2007 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * 	http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package org.intellij.stripes.reference;

import com.intellij.codeInsight.lookup.LookupValueFactory;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.PsiClassReferenceType;
import com.intellij.psi.util.PropertyUtil;
import com.intellij.psi.util.PsiElementFilter;
import com.intellij.psi.util.PsiUtil;
import com.intellij.psi.xml.XmlTag;
import org.intellij.stripes.util.StripesConstants;
import org.intellij.stripes.util.StripesTagFilter;
import org.intellij.stripes.util.StripesUtil;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;
import java.util.*;

/**
 * Util Class
 * Created by IntelliJ IDEA.
 * User: Mario Arias
 * Date: 7/04/2008
 * Time: 04:01:50 AM
 */
public final class StripesReferenceUtil {

    private static List<String> EMPTY_STRING_LIST = new ArrayList<String>(8);

    public static PsiElementFilter NAME_ATTR_FILTER = new StripesTagFilter() {
        protected boolean isDetailsAccepted(XmlTag tag) {
            return tag.getAttributeValue(StripesConstants.NAME_ATTR) != null;
        }
    };

    /**
     * Get the Event methods (Resolution methods) for an ActionBean Class
     *
     * @param psiClass an ActionBean PsiClass
     * @return List with all Resolution Methods names
     */
    public static List<String> getResolutionMethodsNames(PsiClass psiClass) {
        List<String> methodNames = new ArrayList<String>(16);
        for (PsiMethod method : getResolutionMethods(psiClass).values()) {
            String s = resolveHandlesEventAnnotation(method);
            //add the @HandlesEvent value
            methodNames.add(null == s ? method.getName() : s);
        }
        return methodNames;
    }

    /**
     * Get the Event methods (Resolution methods) for an ActionBean Class
     *
     * @param psiClass an ActionBean PsiClass
     * @return List with all Resolution Methods
     */
    public static Map<String, PsiMethod> getResolutionMethods(PsiClass psiClass) {
        Map<String, PsiMethod> psiMethods = new HashMap<String, PsiMethod>(8);

//Add Resultion methods for super classes
        PsiClass superClass;
        try {
            superClass = psiClass.getSuperClass();
        } catch (Exception e) {
            return psiMethods;
        }
        assert superClass != null;

        if (!(Object.class.getName().equals(superClass.getQualifiedName()))) {
            psiMethods.putAll(getResolutionMethods(superClass));
        }

        for (PsiMethod method : psiClass.getMethods()) {
//method return Resolution and have 0 parameters
            PsiType returnType = method.getReturnType();
            if (returnType != null) {
                if (returnType.equalsToText(StripesConstants.STRIPES_RESOLUTION_CLASS) && method.getParameterList().getParametersCount() == 0) {
                    psiMethods.put(method.getName(), method);
                }
            }
        }

        return psiMethods;
    }

    public static String resolveHandlesEventAnnotation(PsiMethod method) {
        String retval = null;
        PsiAnnotation a = method.getModifierList().findAnnotation(StripesConstants.HANDLES_EVENT_ANNOTATION);
        try {
            if (a != null) {
                PsiAnnotationMemberValue psvm = a.findAttributeValue("value");
                if (psvm instanceof PsiLiteralExpression) {
                    retval = StringUtil.stripQuotesAroundValue(psvm.getText());
                } else if (psvm instanceof PsiReferenceExpression) {
                    PsiElement el = psvm.getReference().resolve();
                    if (el instanceof PsiField) {
                        retval = StringUtil.stripQuotesAroundValue(((PsiField) el).getInitializer().getText());
                    }
                }
            }
        } catch (ProcessCanceledException e) {
            //Do nothig, this exception is very common and can be throw for intellij
            //Logger don't be reported or just raise an ugly error
        } catch (Exception e) {
            Logger.getInstance("IntelliStripes").error("Error resolving annotation", e);
        }
        return retval;
    }

    static ElementManipulator<PsiLiteralExpression> getManipulator(PsiLiteralExpression expression) {
        return PsiManager.getInstance(expression.getProject()).getElementManipulatorsRegistry().getManipulator(expression);
    }

    /**
     * Returns list of properties for certain class and its superclasses that have setter method
     *
     * @param psiClass class to examine
     * @param braces   append square braces to the property name or not
     * @return {@link java.util.List java.util.List} of property names
     */
    public static List<String> getWritableProperties(PsiClass psiClass, Boolean braces) {
        if (null == psiClass) return EMPTY_STRING_LIST;

        List<String> methodNames = new ArrayList<String>(16);
        for (PsiMethod psiMethod : psiClass.getAllMethods()) {
            String name = psiMethod.getName();
            if (name.startsWith("set") && psiMethod.getParameterList().getParametersCount() == 1) {
                PsiType propertyType = psiMethod.getParameterList().getParameters()[0].getType();
                PsiClass propertyClass = PsiUtil.resolveClassInType(propertyType);

                if (StripesUtil.isSubclass(StripesConstants.ACTION_BEAN_CONTEXT, propertyClass)
                        || StripesUtil.isSubclass(StripesConstants.FILE_BEAN, propertyClass)) continue;

                name = StringUtil.decapitalize(name.replaceFirst("set", ""));
                if (propertyType instanceof PsiArrayType
                        || StripesUtil.isSubclass(List.class.getName(), propertyClass)
                        || StripesUtil.isSubclass(Map.class.getName(), propertyClass)) {
                    name += (braces ? "[]" : "");
                }
                methodNames.add(name);
            }
        }

        return methodNames;
    }

    /**
     * Returns properties of FileBean of certain PsiClass that can be set by Stripes.
     *
     * @param psiClass
     * @return {@link java.util.List list} of property names
     */
    public static List<String> getFileBeanProperties(PsiClass psiClass) {
        if (null == psiClass) return EMPTY_STRING_LIST;

        List<String> methodNames = new ArrayList<String>(4);
        for (PsiMethod psiMethod : psiClass.getAllMethods()) {
            if (PropertyUtil.isSimplePropertySetter(psiMethod)) {
                PsiType propertyType = psiMethod.getParameterList().getParameters()[0].getType();
                PsiClass propertyClass = StripesReferenceUtil.resolveClassInType(propertyType, psiClass.getProject());

                if (StripesUtil.isSubclass(StripesConstants.FILE_BEAN, propertyClass)) {
                    String methodName = PropertyUtil.getPropertyNameBySetter(psiMethod);

                    propertyClass = PsiUtil.resolveClassInType(propertyType);
                    if (StripesUtil.isSubclass(List.class.getName(), propertyClass)
                            || propertyType instanceof PsiArrayType
                            || StripesUtil.isSubclass(Map.class.getName(), propertyClass)) {
                        methodName += "[]";
                    }
                    methodNames.add(methodName);
                }
            }
        }

        return methodNames;
    }

    public static List<String> splitNestedVar(String var) {
        List<String> retval = new ArrayList<String>(8);
        for (int i = 0, wStart = 0, lBrace = 0; i < var.length(); i++) {
            if (var.charAt(i) == '.' && lBrace == 0) {
                retval.add(var.substring(wStart, i));
                wStart = i + 1;
            } else if (var.charAt(i) == '[') {
                lBrace++;
            } else if (var.charAt(i) == ']') {
                lBrace--;
            }

            if (i == (var.length() - 1)) {
                retval.add(var.substring(wStart, var.length()));
            }
        }
        return retval;
    }

    public static PsiClass resolveActionBeanSetterReturnType(PsiClass host, String field) {
        PsiClass cls;

        try {
            PsiMethod setter = host.findMethodsByName("set" + StringUtil.capitalize(field.replaceAll("\\[.*?\\]", "")), true)[0];
            PsiType propertyType = setter.getParameterList().getParameters()[0].getType();
            cls = resolveClassInType(propertyType, host.getProject());
        } catch (Exception e) {
            cls = null;
        }

        return cls;
    }

    public static PsiClass resolveClassInType(PsiType propertyType, Project project) {
        PsiClass cls;
        try {
            PsiClass propertyClass = PsiUtil.resolveClassInType(propertyType);
            if (StripesUtil.isSubclass(List.class.getName(), propertyClass)) {
                if (((PsiClassReferenceType) propertyType).getParameters().length == 1) {
                    cls = PsiUtil.resolveClassInType(((PsiClassReferenceType) propertyType).getParameters()[0]);
                } else {
                    cls = StripesUtil.findPsiClassByName(Object.class.getName(), project);
                }
            } else if (StripesUtil.isSubclass(Map.class.getName(), propertyClass)) {
                if (((PsiClassReferenceType) propertyType).getParameters().length == 2) {
                    cls = PsiUtil.resolveClassInType(((PsiClassReferenceType) propertyType).getParameters()[1]);
                } else {
                    cls = StripesUtil.findPsiClassByName(Object.class.getName(), project);
                }
            } else {
                cls = propertyClass;
            }
        } catch (Exception e) {
            cls = null;
        }

        return cls;
    }

    /**
     * Gets s PsiClass from a ExpressionList of type com.foo.MyClass.class
     *
     * @param list
     * @return
     */
    public static PsiClass getPsiClassFromExpressionList(PsiExpressionList list) {
        String className = list.getExpressions()[0] instanceof PsiClassObjectAccessExpression
                ? ((PsiClassObjectAccessExpression) list.getExpressions()[0]).getOperand().getType().getCanonicalText()
                : null;

        return StripesUtil.findPsiClassByName(className, list.getProject());
    }

    /**
     * Get an ActionBean PsiClass from a given tag parent
     *
     * @param xmlTag xml tag
     * @param parent parent's name
     * @return An ActionBean PsiClass
     */
    public static PsiClass getBeanClassFromParentTag(@NotNull XmlTag xmlTag, @NotNull String parent) {
        for (XmlTag tag = xmlTag.getParentTag(); tag != null; tag = tag.getParentTag()) {
            if (tag.getNamespace().startsWith(StripesConstants.TAGLIB_PREFIX)
                    && parent.equals(tag.getLocalName())) {
                PsiClass cls = StripesUtil.findPsiClassByName(tag.getAttributeValue(StripesConstants.BEANCLASS_ATTR), tag.getProject());
                return StripesUtil.isSubclass(StripesConstants.ACTION_BEAN, cls) ? cls : null;
            }
        }

        return null;
    }

    public static Object[] getVariants(@NotNull Collection<String> list, @NotNull Icon icon) {
        if (list.isEmpty()) return PsiReferenceBase.EMPTY_ARRAY;

        Object[] retval = new Object[list.size()];
        Integer i = 0;
        for (String s : list) {
            retval[i++] = LookupValueFactory.createLookupValue(s, icon);
        }
        return retval;
    }

    public static UrlBindingSearcher URL_BINDING_SEARCHER;
    private static Map<String, PsiClass> EMPTY_URL_BINDING_MAP = new HashMap<String, PsiClass>(0);

    public static Map<String, PsiClass> getUrlBindings(Project project) {
        if (URL_BINDING_SEARCHER == null) {
            PsiClass urlBindingCls = StripesUtil.findPsiClassByName(StripesConstants.URL_BINDING_ANNOTATION, project);
            if (null != urlBindingCls) URL_BINDING_SEARCHER = new UrlBindingSearcher(urlBindingCls);
        }
        return URL_BINDING_SEARCHER == null ? EMPTY_URL_BINDING_MAP : URL_BINDING_SEARCHER.execute();
    }
}
